package org.activequant.data.util.series;

import java.util.ArrayList;
import java.util.List;

import org.activequant.core.domainmodel.InstrumentSpecification;
import org.activequant.core.domainmodel.SeriesSpecification;
import org.activequant.core.domainmodel.data.MarketDataEntity;
import org.activequant.core.domainmodel.data.TimeSeries;
import org.activequant.core.types.TimeStamp;
import org.activequant.data.preparation.IChain;
import org.activequant.data.retrieval.ISeriesDataSource;

/**
 * Series source which acts as proxy holding multiple series sources and applying data preparation/transformation filters.<br/>
 * An abstract class. Defines a nested Entry class. FilterSeriesSource&lt;U extends MarketDataEntity&lt;U&gt;, T extends TimeSeries&lt;U&gt;&gt; implements ISeriesDataSource&lt;T&gt;.<br/>
 * Holds the following associated variables:
 * <ul>
 * <li>clazz(Class&lt;U&gt;)</li>
 * <li>map(List&lt;Entry&gt;)</li>
 * <li>chain(IChain)</li>
 * <li>data(T[])</li>
 * </ul>
 * <p>
 * <b>History:</b><br>
 *  - [24.02.2008] Created (Erik Nijkamp)<br>
 *
 *  @author Erik Nijkamp
 */
public abstract class FilterSeriesSource<U extends MarketDataEntity<U>, T extends TimeSeries<U>> implements ISeriesDataSource<T> {
	/**
	 * A protected and nested class. Holds the following associated variables:
	 * <ul>
	 * <li>seriesSpec(SeriesSpecification)</li>
	 * <li>dataSource(ISeriesDataSource&lt;T&gt;)</li>
	 * </ul>
	 * @author Dimitar
	 *
	 */
	protected class Entry {
		/**
		 * SeriesSpecification seriesSpec;
		 */
		SeriesSpecification seriesSpec;
		/**
		 * ISeriesDataSource&lt;T&gt; dataSource;
		 */
		ISeriesDataSource<T> dataSource;
		/**
		 * constructs an Entry using the given seriesSpec(SeriesSpecification) and dataSource(ISeriesDataSource&lt;T&gt;) to set its associated 
		 * seriesSpec(SeriesSpecification) and dataSource(ISeriesDataSource&lt;T&gt;)
		 * @param seriesSpec
		 * @param dataSource
		 */
		public Entry(SeriesSpecification seriesSpec, ISeriesDataSource<T> dataSource) {
			this.seriesSpec = seriesSpec;
			this.dataSource = dataSource;
		}
	}
	/**
	 * private final Class&lt;U&gt; clazz;
	 */
	private final Class<U> clazz;
	/**
	 * private List&lt;Entry&gt; map = new ArrayList&lt;Entry&gt;();
	 */
	private List<Entry> map = new ArrayList<Entry>();
	/**
	 * private IChain chain;
	 */
	private IChain chain;
	/**
	 * private T[] data;
	 */
	private T[] data;	
	/**
	 * constructs a FilterSeriesSource&lt;U extends MarketDataEntity&lt;U&gt;, T extends TimeSeries&lt;U&gt;&gt;(implements ISeriesDataSource&lt;T&gt;) using the given
	 * clazz(Class&lt;U&gt;) to set its associated clazz(Class&lt;U&gt;)
	 * @param clazz
	 */
	public FilterSeriesSource(Class<U> clazz) {
		this.clazz = clazz;
	}
	/**
	 * constructs a FilterSeriesSource&lt;U extends MarketDataEntity&lt;U&gt;, T extends TimeSeries&lt;U&gt;&gt;(implements ISeriesDataSource&lt;T&gt;) using the given
	 * clazz(Class&lt;U&gt;) and chain(IChain) to set its associated clazz(Class&lt;U&gt;) and chain(IChain).<br/>
	 * The given seriesSpecifications(SeriesSpecification[]) and dataSources(ISeriesDataSource&lt;T&gt;[]) must have the same length and are being used to
	 * create Entry elements and fill the associated map(List&lt;Entry&gt;) with them.
	 * @param clazz
	 * @param seriesSpecifications
	 * @param dataSources
	 * @param chain
	 */
	public FilterSeriesSource(Class<U> clazz, SeriesSpecification[] seriesSpecifications, ISeriesDataSource<T>[] dataSources, IChain chain) {
		assert(seriesSpecifications.length == dataSources.length);
		this.clazz = clazz;
		for(int i = 0; i < seriesSpecifications.length; i++) {
			map.add(new Entry(seriesSpecifications[i], dataSources[i]));
		}
		this.chain = chain;
	}
	/**
	 * sets the associated chain(IChain) with the given chain(IChain)
	 * @param chain
	 */
	public void setChain(IChain chain) {
		this.chain = chain;
	}
	/**
	 * an abstract method
	 * @param list
	 * @return
	 */
	protected abstract T[] toArray(List<T> list);
	/**
	 * iterates the associated map(List&lt;Entry&gt;) and for each Entry's dataSource-seriesSpec pair it gets a result(T) for that
	 * seriesSpec(SeriesSpecification) using the dataSource(ISeriesDataSource&lt;T&gt;).
	 * The results(T[]) are then processed with the associated chain(IChain) and clazz(Class&lt;U&gt;) and the filtered is result array is used to set the associated data(T[]).
	 * @throws Exception
	 */
	public void prefetch() throws Exception {
		// fetch series data
		List<T> series = new ArrayList<T>(); 
		for(Entry entry : map) {
			ISeriesDataSource<T> source = entry.dataSource;
			SeriesSpecification spec = entry.seriesSpec;
			T result = source.fetch(spec);
			series.add(result);
		}
		// transform data
		data = toArray(series);
		data = chain.process(clazz, data);
	}
	/**
	 * Iterates the associated data(T[]) and looks for the one whose instrumentSpecification(InstrumentSpecification) matches the 
	 * InstrumentSpecification of the given seriesSpecification(SeriesSpecification).<br/>
	 * Once it finds that T(TimeSeries subclass) it returns a sublist TimeSeries with the elements whose timeStamp is in the startTimeStamp-endTimeStamp interval of the 
	 * given seriesSpecification(SeriesSpecification).
	 */
	@SuppressWarnings("unchecked")
	public T fetch(SeriesSpecification seriesSpecification) throws Exception {
		// prefetch data if not cached
		if(data == null) {
			prefetch();
		}
		// find specification
		for(T ts : data) {
			// extract instruments
			InstrumentSpecification fetchSpec = seriesSpecification.getInstrumentSpecification();
			InstrumentSpecification seriesSpec = ts.getInstrumentSpecification();
			// compare
			if(fetchSpec.equals(seriesSpec)) {
				TimeStamp start = seriesSpecification.getStartTimeStamp();
				TimeStamp end = seriesSpecification.getEndTimeStamp();
				T sublist = (T) ts.subList(start, end);
				return sublist;
			}
		}
		throw new IllegalArgumentException("Specification unknown.");
	}

}
